/**
*    Copyright (C) 2008  Chase Kernan 
*    chase.kernan@gmail.com
*
*    This program is free software: you can redistribute it and/or modify
*    it under the terms of the GNU General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
* 
*/

package com.chasekernan.hxnova.core.stars;

import com.chasekernan.hxnova.core.components.ComponentSet;
import com.chasekernan.hxnova.core.components.Defense;
import com.chasekernan.hxnova.core.components.Hull;
import com.chasekernan.hxnova.core.components.Scanner;
import com.chasekernan.hxnova.core.components.StarScanner;
import com.chasekernan.hxnova.core.dataholders.Global;
import com.chasekernan.hxnova.core.minerals.MineralSet;
import com.chasekernan.hxnova.core.players.Player;
import com.chasekernan.hxnova.core.races.PRTSet;
import com.chasekernan.hxnova.core.designs.HullDesign;
import com.chasekernan.hxnova.core.dataholders.Players;
import com.chasekernan.hxnova.core.SpaceObject;
import com.chasekernan.hxnova.utils.Utils;
import com.chasekernan.hxnova.core.fleets.Fleet;
import com.chasekernan.hxnova.utils.Vector;
import com.chasekernan.hxnova.core.races.Race;
import com.chasekernan.hxnova.utils.XmlBuilder;

/**
    Enum describing the starbase of a star system.
    If it is null, the starbase isn't scanned, otherwise it is one of the enum
    options.
**/
enum StarStarbase {
    Design(d : HullDesign);
    None;
}

/**
    Contains all of the functions and data related to star systems.
    Data that is unkown is represented as null.
**/
class Star extends SpaceObject {
    
    /**
        Population growth decrease based on overpopulation.
    **/
    public static var CROWDING_FACTOR : Float = 1.75;
    
    /**
        The amount of minerals mined to decrease the concentration by 1.
    **/
    public static var CONCENTRATION_DECREASE : Float = 12500.0;
    
    /**
        Factory and mine data are often treated as blocks of this size.
    **/
    public static var MINE_AND_FACTORY_BLOCK : Int = 10;
    
    /**
        Population is also often treated as blocks of this size.
    **/
    public static var POPULATION_BLOCK_SIZE : Int = 10000;
    
    public static var MAX_NAME_LENGTH : Int = 20;
    
    public static var MAX_DEFENSES : Int = 100;
    
    public static var MAX_POPULATION : Int = 1000000;
    
    /**
        Stars are sorted by id, not name.
    **/
    public var isHomeworld : Null<Bool>;
    public var name : String;
    public var environment : Environment;
    public var concentrations : Concentrations;
    
    /**
        If no player owns the star, then there is a static 'nature' player
        that is specified.
    **/
    public var owner : Player;
    
    /**
        The fleets orbiting the star are stored here for ease of access and this
        is not saved.
    **/
    public var orbitingFleets : Array<Fleet>;
    public var factories : Null<Int>;
    public var mines : Null<Int>;
    public var defenses : Null<Int>;
    public var population : Null<Int>;
    public var hasScanner : Null<Bool>;
    public var starbase : StarStarbase;
    public var minerals : MineralSet;
    public var production : ProductionQueue;
    public var scannedDefenseCoverage : Null<Float>;
    
    /**
        [additionalResources] is used for adding resources from things like 
        scrapping to the available production resources. It is never saved,
        and it is initialized to zero every turn.
    **/
    public var additionalResources : Int;
    
    public function new() {
        super();
        additionalResources = 0;
    }
    
    /**
        Returns the hability of a star for a given race (without terraforming).
        Based on the formula given at starsautohost (thanks to m.a@stars and 
        ConstB).
        
        Value returned is from -0.45 to 1.0
        
        If [race] isn't given and the star has an owner, then the owner's race
        is used.
        
        NOTE: This depends on having 3 env types.
    **/
    public function getHabPercent(?race : Race) : Float {
        if (Environment.ENV_TYPES.length != 3) {
            throw "Environment types needs to have exactly 3 types.";
        }
        
        if (race == null) {
            if (owner == null || 
                        owner.id == Global.players.nullPlayer.id) {
                throw "If race isn't provided then the star needs to have an " + 
                      "owner for [getHabPercent].";
            } else {
                race = owner.race;
            }
        }
        
        var starValuePoints = 0;
        var redValue = 0;
        var ideality = 10000;

        for (type in Environment.ENV_TYPES) {
            var habPart = race.habSetting.getHabPart(type);
            var habUpper = habPart.upperBound;
            
            //no immunity implemented yet.
            //if (IMMUNE(habUpper)) {			//perfect hab
            //    starValuePoints += 10000;
            //} else {	//the messy part
            
            var habLower  = habPart.lowerBound;
            var planetHab = environment.getValue(type);
            
            var distToCenter = Std.int(Math.abs(planetHab - habPart.middle));
            var habRadius = Std.int(habPart.radius);
            
            if (distToCenter <= habRadius) { //green star
                //excentr is supposedly some kind of excentricity, but reversed
                var excentr = Std.int(100 * distToCenter / habRadius);
                excentr = 100 - excentr;
                
                starValuePoints += excentr * excentr;
                
                var margin = distToCenter * 2 - habRadius;
                
                //if so hab is in the "external quarters". 
                //dist2Center > 0.5*habRadius
                if (margin > 0) {
                    //decrease ideality up to ~50%
                    ideality = Std.int(
                            ideality * (3 / 2 - distToCenter / habRadius));
                }
            } else { //its a red star
                var negative = distToCenter - habRadius;
                if (negative > 15) negative = 15;
                redValue += negative;
            }
        }
        
        if (redValue != 0) return -redValue / 100.0;
        
        starValuePoints = Std.int(Math.sqrt(starValuePoints / 3) + 0.9);
        starValuePoints = Std.int(starValuePoints * ideality / 10000);
        
        return starValuePoints / 100.0;
    }
    
    /**
        Returns true if the star is inhabited and the population is known.
    **/
    public function isInhabited() : Bool {
        return population != null && population > 0;
    }
    
    /**
        Returns the number of factories that could possibly built on the star
        but they are not necessarily usable.
    **/
    public function getPossibleFactories() : Int {
        return Std.int(population / POPULATION_BLOCK_SIZE 
                * owner.race.operableFactories);
    }
    
    /**
        Returns the number of mines that could possibly built on the star
        but they are not necessarily usable.
    **/
    public function getPossibleMines() : Int {
        return Std.int(population / POPULATION_BLOCK_SIZE *
                owner.race.operableMines);
    }
    
    /**
        Returns the number of factories that the current population could use.
    **/
    public function getUsableFactories() : Int {
        return Std.int(Math.min(getPossibleFactories(), factories));
    }
    
    /**
        Returns the number of mines that the current population could use.
    **/
    public function getUsableMines() : Int {
        return Std.int(Math.min(getPossibleMines(), mines));
    }
    
    /**
        Returns the number of possible defenses.
        TODO: Actually implement the real formula.
    **/
    public function getPossibleDefenses() : Int {
        return Math.ceil(
                Math.max(0, MAX_DEFENSES * getHabPercent(owner.race)));
    }
    
    /**
        Returns the number of defenses that the current population can use.
        TODO: Actually implement this.
    **/
    public function getUsableDefenses() : Int {
        return getPossibleDefenses();
    }
    
    /**
        Checks to see if the star is unihabitted. If it is, the star loses its
        owner, defenses, and scanner.
    **/
    public function checkPopulation() {
        if (population != null && population <= 0) {
            owner = Global.players.nullPlayer;
            defenses = 0;
            population = 0;
            hasScanner = false;
        }
    }
    
    /**
        Returns how much the given population will grow in one year.
        If [pop] isn't given, then the star's population is used.
        
        Based off of the formula used in Nova (thanks to Ken Reed).
    **/
    public function calculateGrowth(?pop : Int) : Int {
        if (pop == null) pop = population;
        
        var hab = getHabPercent();
        if (hab < 0) return Std.int((hab / 10.0) * pop);
        
        var populationGrowth : Float = pop * PRTSet.getGrowthRate(
                owner.race.prt) * (owner.race.growthRate / 100.0) * 
                getHabPercent();
        var capacity : Float = pop / PRTSet.getMaxPop(owner.race.prt) * hab;
                
        
        if (capacity > 0.25) populationGrowth *= CROWDING_FACTOR * 
                (1.0 - capacity) * (1.0 - capacity);
        
        return Std.int(populationGrowth);
    }
    
    /**
        Returns null if the star has no scanner, otherwise it returns the
        scanner its owner's race currently uses.
    **/
    public function getScanner() : StarScanner {
        if (hasScanner == null || !hasScanner || owner == null || 
                owner.id == Global.players.nullPlayer.id)  {
            return null;
        }
        
        return owner.starScanner;
    }
    
    /**
        Returns null if there if no one owns the star, otherwise, it returns
        the defense type of the current owner.
    **/
    public function getDefenseType() : Defense {
        if (owner == null || owner.id == Global.players.nullPlayer.id) {
            return null;
        }
        
        return owner.defenseType;
    }
    
    /**
        Calculates the available resources of a given star system.
        If years are specified, it (kinda) calcs the resources in a certain year 
        (relative to the present) in the future. Currently it only deals with
        the pop growth. 
        
        It will throw an error if the years < 0 or if the star is uninhabited.
        
    **/
    public function calculateResources(?year = 0) : Int {
        if (year < 0) throw "Year must be greater than 0.";
        if (!isInhabited()) throw "Star must be populated.";
        
        var newPop = population;
        for (i in 0...year) newPop += calculateGrowth(newPop);
        if (newPop <= 0) return 0;
        
        var res = newPop / owner.race.popProduction;
        res += getUsableFactories() / MINE_AND_FACTORY_BLOCK * 
               owner.race.factoryProduction + additionalResources;
        
        return Std.int(res);
    }
    
    /**
        Returns the amount of minerals the star can mine this year.
        
        Will throw an error if the star isn't habited.
        
        Based off of the formula used in Nova (thanks to Ken Reed).
    **/
    public function getMineralsMined() : MineralSet {
        if (!isInhabited()) throw "Star must be populated.";
        
        var mins = new MineralSet();
        for (type in Concentrations.MINERAL_TYPES) {
            var mined = Std.int(((getUsableMines() / MINE_AND_FACTORY_BLOCK) * 
                    owner.race.mineProduction) * concentrations.getValue(type) 
                    / 100.0);
            
            mins.setValue(type, mined);
        }
        
        return mins;
    }
    
    /**
        Returns the amount of minerals mined by the star system and decreases
        the concentrations accordingly.
        
        Will throw an error if the star isn't habited.
        
        Based off of the formula used in Nova (thanks to Ken Reed).
    **/
    public function mineMinerals() : MineralSet {
        if (!isInhabited()) throw "Star must be populated.";
        
        var mins = getMineralsMined();
        for (type in Concentrations.MINERAL_TYPES) {
            var conc = concentrations.getValue(type);
            var mined = mins.getValue(type);
            
            var newConc = Math.max(conc - mined / (12500.0 / conc),
                    if (isHomeworld) Concentrations.MIN_HW_VALUE 
                    else Concentrations.MIN_VALUE);
            
            concentrations.setValue(type, newConc);
        }
        
        return mins;
    }
    
    public static var XML_TITLE = "star";
    
    /**
        Writes a star to an xml file.
        Sample xml:
            TODO: run writeToXml and insert xml here.
    **/
    public static function writeToXml(star : Star) : Xml {
        var xb = new XmlBuilder(XML_TITLE);
        
        //note: theses are automatically read from the star object, then 
        //converted to the underscore version.
        xb.addAttributes(star, ["name", "id", "isHomeworld", "hasScanner"]);
        xb.addAttributes(star.location, ["x", "y"]);
        if (star.owner != null) xb.addAttribute("owner_id", star.owner.id);
        
        xb.addClassObj(star.environment);
        xb.addClassObj(star.concentrations);
        
        xb.addValueChild("factories", star.factories);
        xb.addValueChild("mines", star.mines);
        xb.addValueChild("defenses", star.defenses);
        xb.addValueChild("population", star.population);
        xb.addValueChild("scanned_defense_coverage", 
                         star.scannedDefenseCoverage);
        
        if (star.starbase != null) {
            xb.addValueChild("starbase", switch(star.starbase) {
                
                case None:
                    "none";
                    
                case Design(d):
                    "design-" + d.name;
            });
        }
        
        xb.addClassObj(star.production);
        xb.addClassObj(star.minerals);
        
        return xb.xml;
    }
    
    /**
        Reads from a star xml file.
        Will throw an error if there are any mistakes.
    **/
    public static function readFromXml(xml : Xml) : Star {
        try {
            var s = new Star();
            var xb = XmlBuilder.fromXml(xml);
            
            s.name = xb.readStringAttribute("name");
            s.id = xb.readIntAttribute("id");
            s.location = new Vector(xb.readIntAttribute("x"), 
                                    xb.readIntAttribute("y"));
            s.hasScanner = xb.readBoolAttribute("hasScanner");
            s.hasScanner = xb.readBoolAttribute("isHomeworld");
            
            var ownerId = xb.readIntAttribute("ownerId");
            if (ownerId != null) {
                s.owner = Global.players.getPlayerById(ownerId);
            }
            
            s.environment = xb.readClassObj(Environment);
            s.concentrations = xb.readClassObj(Concentrations);
            
            //Std.parseInt returns null if it's given null
            s.factories = Std.parseInt(xb.readValueChild("factories"));
            s.mines = Std.parseInt(xb.readValueChild("mines"));
            s.defenses = Std.parseInt(xb.readValueChild("defenses"));
            s.population = Std.parseInt(xb.readValueChild("population"));
            
            //throwing an error here for some reason...
            var scanned = xb.readValueChild("scanned_defense_coverage");
            if (scanned != null) {
                s.scannedDefenseCoverage = Std.parseFloat(scanned);
            }
            
            var sbData = xb.readValueChild("starbase");
            if (sbData != null) {
                if (sbData == "none") s.starbase = None;
                else if (s.owner == null) {
                    throw "Star owner must be known in order to know the " + 
                          "starbase design.";
                } else s.starbase = Design(s.owner.getDesignByName(
                                           sbData.substr("design-".length)));
            }
            
            s.production = xb.readClassObj(ProductionQueue);
            if (s.production != null) s.production.parent = s;
            
            s.minerals = xb.readClassObj(MineralSet);
            
            return s;
        } catch (e : Dynamic) {
            throw "Error while reading from star xml file: " + e;
        }
    }
    
    /**
        Returns just the id, name, and location of the star.
        Used to compile the map info.
    **/
    public function cloneMapPart() : Star {
        var s : Star = new Star();
        s.id = id;
        s.name = name;
        s.location = location.clone();
        return s;
    }
    
    /**
        Writes only the map information carried by a star object.
        Example:
            <star name="name" id="43" x="34" y="34"/>
    **/
    public static function writeMapPartToXml(s : Star) : Xml {
        var xb = new XmlBuilder(XML_TITLE);
        
        xb.addAttributes(s, ["name", "id"]);
        xb.addAttributes(s.location, ["x", "y"]);
        
        return xb.xml;
    }
    
    /**
        Reads only the map information from an xml file.
    **/
    public static function readMapPartFromXml(xml : Xml) : Star {
        var s = new Star();
        var xb = XmlBuilder.fromXml(xml);
        
        s.name = xb.readStringAttribute("name");
        s.id = xb.readIntAttribute("id");
        s.location = new Vector(xb.readIntAttribute("x"), 
                                xb.readIntAttribute("y"));
        return s;
    }
}

#if unit

import com.chasekernan.hxnova.core.players.Player;
import com.chasekernan.hxnova.core.dataholders.Players;

class StarTest extends haxe.unit.TestCase {
    
    var s : Star;
    
    public function new() {
        super();
    }
    
    override public function setup() {
        Global.players = new Players();
        s = new Star();
        s.owner = new Player();
        s.owner.race = new Race();
    }
    
    private static function equals(f1 : Float, f2 : Float) : Bool {
        return Math.abs(f1 - f2) < 0.01;
    }
    
    public function testGetHabPercent() {
        s.environment = new Environment(false);
        assertTrue(equals(1.0, s.getHabPercent(s.owner.race)));
        
        for (i in 0...1000) {
            s.environment = new Environment(true);
            var p = s.getHabPercent(s.owner.race);
            assertTrue(p >= -0.45 && p <= 1.0);
        }
        
        //TODO: Use some hardcoded numbers here
    }
    
    public function testIsInhabited() {
        assertFalse(s.isInhabited());
        s.population = 100;
        assertTrue(s.isInhabited());
        s.population = 0;
        assertFalse(s.isInhabited());
    }
    
    public function testGetPossibleFactories() {
        s.population = Star.POPULATION_BLOCK_SIZE;
        assertEquals(s.owner.race.operableFactories, s.getPossibleFactories());
        
        s.population = Math.round(2.5 * Star.POPULATION_BLOCK_SIZE);
        assertEquals(2.5 * s.owner.race.operableFactories, 
                     s.getPossibleFactories());
    }
    
    public function testGetPossibleMines() {
        s.population = Star.POPULATION_BLOCK_SIZE;
        assertEquals(s.owner.race.operableMines, s.getPossibleMines());
        
        s.population = Math.round(2.5 * Star.POPULATION_BLOCK_SIZE);
        assertEquals(2.5 * s.owner.race.operableMines, s.getPossibleMines());
    }
    
    public function testGetPossibleDefenses() {
        s.environment = new Environment(false);
        assertTrue(equals(Star.MAX_DEFENSES, s.getPossibleDefenses()));
        
        for (i in 0...1000) {
            s.environment = new Environment(true);
            var d = s.getPossibleDefenses();
            assertTrue(d >= 0 && d <= Star.MAX_DEFENSES);
        }
    }
    
    public function testCheckPopulation() {
        s.defenses = 10;
        s.hasScanner = true;
        
        s.checkPopulation();
        assertEquals(10, s.defenses);
        
        s.population = 100;
        assertEquals(10, s.defenses);
        
        s.population = 0;
        s.checkPopulation();
        assertEquals(0, s.defenses);
        assertFalse(s.hasScanner);
        assertEquals(Global.players.nullPlayer, s.owner);
    }
    
    public function testCalculateGrowth() {
        s.population = 1000;
        s.owner.race.growthRate = 10;
        s.environment = new Environment(false);
        
        assertTrue(equals(1000 * 0.2, s.calculateGrowth())); //hyper-expansion
        
        s.population = Std.int(PRTSet.getMaxPop(s.owner.race.prt) / 2);
        var growth = s.population * 0.2;
        growth *= Star.CROWDING_FACTOR * 0.5 * 0.5;
        assertEquals(Std.int(growth), s.calculateGrowth());
        
        for (type in Environment.ENV_TYPES) s.environment.setValue(type, 0);
        
        growth = s.population * s.getHabPercent() / 10;
        assertEquals(Std.int(growth), s.calculateGrowth());
    }
    
    public function testGetScanner() {
        s.hasScanner = null;
        assertEquals(null, s.getScanner());
        s.hasScanner = false;
        assertEquals(null, s.getScanner());
        
        s.hasScanner = true;
        s.owner.starScanner = new StarScanner("Test Scanner");
        assertEquals("Test Scanner", s.getScanner().name);
        
        s.owner = null;
        assertEquals(null, s.getScanner());
    }
    
    public function testGetDefenseType() {
        s.owner.defenseType = new Defense("Test Defense");
        assertEquals("Test Defense", s.getDefenseType().name);
        
        s.owner = null;
        assertEquals(null, s.getDefenseType());
    }
    
    public function testCalculateResources() {
        try {
            s.calculateResources(-1);
            assertTrue(false);
        } catch(e : Dynamic) assertTrue(true);
        
        try {
            s.population = 0;
            s.calculateResources();
            assertTrue(false);
        } catch(e : Dynamic) assertTrue(true);
        
        s.factories = Star.MINE_AND_FACTORY_BLOCK * 5;
        s.population = s.owner.race.popProduction * 50;
        
        assertEquals(5 * s.owner.race.factoryProduction + 50, 
                     s.calculateResources());
        
        s.environment = new Environment(false);
        var newPop = s.population;
        for (i in 0...3) newPop += s.calculateGrowth(newPop);
        
        assertEquals(Math.floor(5 * s.owner.race.factoryProduction +
                     newPop / s.owner.race.popProduction), 
                     s.calculateResources(3));
    }
    
    //TODO: Finish star unit tests
    
}

#end